package com.webex.ta.hydra.gui.table;

import com.webex.ta.hydra.Constants;
import com.webex.ta.hydra.exceptions.IllegalKeywordException;
import com.webex.ta.hydra.core.*;
import com.webex.ta.hydra.gui.GuiPackage;
import com.webex.ta.hydra.gui.table.celleditor.*;
import com.webex.ta.hydra.gui.table.cellrenderer.BreakPointRenderer;
import com.webex.ta.hydra.gui.table.cellrenderer.HydraDefaultTableCellRenderer;
import com.webex.ta.hydra.gui.table.cellrenderer.KeywordRenderer;
import com.webex.ta.hydra.gui.table.undo.ChangeEdit;
import com.webex.ta.hydra.gui.table.undo.InsertEdit;
import com.webex.ta.hydra.gui.table.undo.RemoveEdit;

import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.table.*;
import javax.swing.undo.UndoManager;
import javax.swing.undo.CompoundEdit;
import java.awt.*;
import java.awt.event.*;
import java.util.EventObject;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by Cisco WebEx.
 * User: vegaz
 * Date: 2010-8-18
 * Time: 11:56:43
 */
public class HydraTable extends JTable implements ActionListener, HydraTableModelListener {
    private static BreakPointRenderer breakpoint_renderer = new BreakPointRenderer();
    private boolean locked = false;
//    private final Timer copyHintTimer = new Timer(200, this);
    private Set<CellEditorListener> cellEditorListeners = new HashSet<CellEditorListener>();
    private UndoManager undoManager = new UndoManager();
    private AnnotationEditPane annotationEditor = new AnnotationEditPane(this);
    private boolean annotationEditing = false;
    private boolean annotationShowing = false;

    public HydraTable() {
        super(new HydraTableModel(), new HydraTableColumnModel());
        setAutoCreateColumnsFromModel(true);
        setUI(new HydraTableUI());
//        setUI(new DragDropRowTableUI());

        // ui related settings
        getColumnModel().getColumn(0).setResizable(false);
        getColumnModel().getColumn(0).setMaxWidth(20);
        getColumnModel().getColumn(0).setMinWidth(20);
        setRowHeight(20);
        getTableHeader().setReorderingAllowed(false);
        setCellSelectionEnabled(true);
        setSurrendersFocusOnKeystroke(true);
        setAutoResizeMode(AUTO_RESIZE_OFF);
        setShowGrid(false);
        setDragEnabled(true);
        setDropMode(DropMode.ON_OR_INSERT_ROWS);

        registerKeyboardAction(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (HydraTable.this.getSelectedRow() >= 0 && HydraTable.this.getSelectedColumn() >= 0) {
                    editCellAt(HydraTable.this.getSelectedRow(), HydraTable.this.getSelectedColumn());
                }
            }
        }, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        registerKeyboardAction(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (HydraTable.this.isEditing() && HydraTable.this.getCellEditor() != null)
                    HydraTable.this.getCellEditor().cancelCellEditing();
            }
        }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        addKeyListener(new HydraTableKeyHandler(this));
        setTransferHandler(new HydraTableTransferHandler(this));
        getModel().addHydraTableModelListener(this);

        annotationEditor.addCellEditorListener(new CellEditorListener() {
            public void editingCanceled(ChangeEvent e) {
                annotationEditing = false;
                annotationShowing = false;
                HydraTable.this.requestFocus();
            }

            public void editingStopped(ChangeEvent e) {
                annotationEditing = false;
                annotationShowing = false;
                HydraTable.this.requestFocus();
            }
        });
//        copyHintTimer.start();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
    }

    @Override
    public void createDefaultColumnsFromModel() {
        TableModel m = getModel();
        if (m != null) {
            // Remove any current columns
            TableColumnModel cm = getColumnModel();
            while (cm.getColumnCount() > 0) {
                cm.removeColumn(cm.getColumn(0));
            }

            // Create new columns from the data model info
            for (int i = 0; i < m.getColumnCount(); i++) {
                HydraTableColumn newColumn = new HydraTableColumn(i, 150);
                addColumn(newColumn);
            }
        }

    }

    @Override
    public TableCellRenderer getCellRenderer(int row, int column) {
        if (column == Constants.COLUMN_BREAKPOINT)
            return breakpoint_renderer;
        else if (column == Constants.COLUMN_KEYWORD)
//                return super.getCellRenderer(row, column);
            return new KeywordRenderer();
        return new HydraDefaultTableCellRenderer();
    }

    @Override
    public boolean editCellAt(int row, int column, EventObject e) {
        if (locked)
            return false;

        if (cellEditor != null && !cellEditor.stopCellEditing()) {
            return false;
        }

        if (row < 0 || row >= getRowCount() ||
                column < 0 || column >= getColumnCount()) {
            return false;
        }

        if (!isCellEditable(row, column))
            return false;

        TableCellEditor editor = getCellEditor(row, column);
        if (editor != null && editor.isCellEditable(e)) {
            editorComp = prepareEditor(editor, row, column);
            if (editorComp == null) {
                removeEditor();
                return false;
            }
            editorComp.setBounds(getCellRect(row, column, false));
            add(editorComp);
            editorComp.validate();
            editorComp.repaint();

            setCellEditor(editor);
            setEditingRow(row);
            setEditingColumn(column);
            editor.addCellEditorListener(this);

            if (editor instanceof HydraCellEditor)
                ((HydraCellEditor) editor).startCellEditing();
            return true;
        }
        return false;
    }

//    @Override
//    public void tableChanged(TableModelEvent e) {
//        super.tableChanged(e);
////        if (e.getType() == TableModelEvent.INSERT) {
//        try {
//            Command cmd = this.getModel().getCommandAt(e.getLastRow());
//            this.fireCommandAdded(cmd);
//        } catch (Exception e1) {}
////        }
//    }

    @Override
    public void editingStopped(ChangeEvent e) {
        TableCellEditor editor = getCellEditor();
        if (editor != null) {
            Object value = editor.getCellEditorValue();
            Command oldValue = this.getModel().getCommandAt(editingRow).clone();
            oldValue.setUuid(this.getModel().getCommandAt(editingRow).getUuid());
            setValueAt(value, editingRow, editingColumn);
            Command newValue = this.getModel().getCommandAt(editingRow).clone();
            newValue.setUuid(this.getModel().getCommandAt(editingRow).getUuid());
            fireCellEditingStopped();
            removeEditor();


            // TODO check LoadMacro
            checkMacroLoad(oldValue, newValue);

            if (!oldValue.isEqualWith(newValue)) {
                undoManager.addEdit(new ChangeEdit(this.getModel(), oldValue, newValue));
            }
            TestPlanValidator.validateTestPlan(getModel().getTestPlan());
        }
    }

    public static void checkMacroLoad(Command oldValue, Command newValue) {
        if ("LoadMacro".equals(oldValue.getKeyword()) &&
                !"LoadMacro".equals(newValue.getKeyword())) {
            // unload macro definition
            KeywordRegistry.getInstance().unloadMacros(oldValue.getParameter(0));
        } else if (!"LoadMacro".equals(oldValue.getKeyword()) &&
                "LoadMacro".equals(newValue.getKeyword())) {
            // load macro definition
            KeywordRegistry.getInstance().loadMacros(newValue.getParameter(0));
        } else if ("LoadMacro".equals(oldValue.getKeyword()) &&
                "LoadMacro".equals(newValue.getKeyword())) {
            if (newValue.getParameter(0) != oldValue.getParameter(0)) {
                // unload the old one then load the new one
                KeywordRegistry.getInstance().unloadMacros(oldValue.getParameter(0));
                KeywordRegistry.getInstance().loadMacros(newValue.getParameter(0));
            }
        }
    }


    @Override
    public TableCellEditor getCellEditor(int row, int column) {
        if (column == Constants.COLUMN_BREAKPOINT) {
            return new BreakPointEditor();
        } else if (column == Constants.COLUMN_KEYWORD)
//            return new KeywordEditor(this);
            return new KeywordEditor2();
        else {
            Command cmd = this.getModel().getCommandAt(row);
            String kw = cmd.getKeyword();
            try {
                int idx = column - 2;
                Keyword keyword = KeywordRegistry.getInstance().createKeywordOrMacro(kw);
                return keyword.getParameterEditor(cmd, idx);
            } catch (Exception ignore) {
            }

            return new HydraDefaultTableCellEditor(new JTextField());
        }
    }

    @Override
    public void removeEditor() {
        TableCellEditor editor = getCellEditor();
        if (editor instanceof HydraTableCellEditor) {
            ((HydraTableCellEditor)editor).beforeRemove();
        }
        super.removeEditor();
    }

    @Override
    public HydraTableModel getModel() {
        return (HydraTableModel) dataModel;
    }

    @Override
    public void setModel(TableModel dataModel) {
        if (!(dataModel instanceof HydraTableModel))
            throw new RuntimeException("HydraTable only can use HydraTableModel as the data model ");
        super.setModel(dataModel);
    }

    @Override
    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
        if (e.getKeyCode() == KeyEvent.VK_DELETE) {
            this.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
            boolean retvalue = super.processKeyBinding(ks, e, condition, pressed);
            this.putClientProperty("JTable.autoStartsEdit", Boolean.TRUE);
            return retvalue;
        } else if (e.getModifiers() == InputEvent.CTRL_MASK) {
//            if(e.getKeyCode() == KeyEvent.VK_C || e.getKeyCode() == KeyEvent.VK_V ||
//                    e.getKeyCode() == KeyEvent.VK_X)
            return false;
        }
        return super.processKeyBinding(ks, e, condition, pressed);
    }

    @Override
    public void valueChanged(ListSelectionEvent e) {
        super.valueChanged(e);
        GuiPackage.getInstance().updateMenuAndToolBar();
    }

    public void stopEdit() {
        if (this.isEditing()) {
            this.getCellEditor().stopCellEditing();
        }
    }

    public void toggleExpanded(final int row) {
        Command cmd = this.getModel().getCommandAt(row);
        if (cmd.hasChild()) {
            if (cmd.isExpanded()) {
                collapse(row);
            } else {
                expand(row);
            }
        }
    }

    public void expand(final int row) {
        Command cmd = this.getModel().getCommandAt(row);
        if (cmd.hasChild() && !cmd.isExpanded()) {
            getModel().expand(cmd);
            getSelectionModel().setLeadSelectionIndex(row);
        }
    }

    public void collapse(final int row) {
        Command cmd = this.getModel().getCommandAt(row);
        if (cmd.hasChild() && cmd.isExpanded()) {
            getModel().collapse(cmd);
            getSelectionModel().setLeadSelectionIndex(row);
        }
    }

    public void makeVisible(Command cmd) {
        // first we need to expand to cmd
        expandTo(cmd);
        this.scrollRectToVisible(this.getCellRect(this.getModel().indexOf(cmd), 0, false));
    }

    public void adjustColumnCount() {
        LongestCommandFinder finder = new LongestCommandFinder();
        this.getModel().getRoot().traverse(finder);
        // don't forget keyword and breakpoint
        int gap = finder.longest + 2 - this.getColumnCount();
        if (gap > 0) {
            for (int i = 0; i < gap; i++) {
                this.addColumn(new TableColumn(this.getColumnCount()));
            }
        }


    }

    private void expandTo(Command cmd) {
        HydraTableModel model = this.getModel();
        if (model.indexOf(cmd) < 0) { // cmd is not visible in table
            expandTo(cmd.getParent());
            expandTo(cmd);
        } else {
            model.expand(cmd);
        }
    }


    public void lock() {
        locked = true;
        this.getModel().lock();
    }

    public void unlock() {
        locked = false;
        getModel().unlock();
    }

    public boolean isLocked() {
        return locked;
    }

    public void addCellEditorListener(CellEditorListener listener) {
        cellEditorListeners.add(listener);
    }

    protected void fireCellEditingStopped() {
        for (CellEditorListener listener : cellEditorListeners) {
            listener.editingStopped(new ChangeEvent(this));
        }
    }


    public void actionPerformed(ActionEvent e) {
        this.repaint();
    }

    public void resetUndoManager() {
        undoManager.discardAllEdits();
    }

    public void undo() {
        undoManager.undo();
    }

    public void redo() {
        undoManager.redo();
    }

    public boolean canUndo() {
        return undoManager.canUndo();
    }

    public boolean canRedo() {
        return undoManager.canRedo();
    }

    public void showAnnotationAt(int row) {
        if (annotationShowing)
            annotationEditor.hide();

        annotationShowing = true;
        Command editingCmd = this.getModel().getCommandAt(row);
        annotationEditor.setCommand(editingCmd);
        Rectangle rect = this.getCellRect(row, Constants.COLUMN_KEYWORD, false);
        Point pt = new Point(rect.x + rect.width, rect.y - rect.height);
        pt = SwingUtilities.convertPoint(this, pt, SwingUtilities.getWindowAncestor(this));
//        pt = SwingUtilities.convertPoint(this, pt, this.getRootPane());
        annotationEditor.show(pt);
    }

    public void hideAnnotation() {
        annotationEditor.hide();
    }

    public void editAnnotationAt(int row) {
        if (annotationEditing)
            stopAnnotationEditing();

        annotationEditing = true;
        showAnnotationAt(row);
        annotationEditor.focus();
    }

    public void stopAnnotationEditing() {
        annotationEditing = false;
        annotationEditor.stopEdit();
        requestFocus();
    }

    public boolean isAnnotationEditing() {
        return annotationEditing;
    }

    public AnnotationEditPane getAnnotationEditor() {
        return annotationEditor;
    }

    public void commandAdded(Command parent, Command added, int pos) {
        undoManager.addEdit(new InsertEdit(this.getModel(), parent, added, pos));
        GuiPackage.getInstance().updateMenuAndToolBar();
    }

    public void commandRemoved(Command parent, Command removed, int pos) {
        undoManager.addEdit(new RemoveEdit(this.getModel(), parent, removed, pos));
        GuiPackage.getInstance().updateMenuAndToolBar();
    }

    public void commandUpdated(Command oldCommand, Command newCommand) {
        undoManager.addEdit(new ChangeEdit(this.getModel(), oldCommand, newCommand));
        GuiPackage.getInstance().updateMenuAndToolBar();
    }

    public void commandsAdded(Command parent, Command[] added, int pos) {
        CompoundEdit edit = new CompoundEdit();
        for (int i = added.length-1; i >=0; i--) {
            edit.addEdit(new InsertEdit(this.getModel(), parent, added[i], pos));
        }
        edit.end();
        undoManager.addEdit(edit);
        GuiPackage.getInstance().updateMenuAndToolBar();
    }

    public void commandsRemoved(Command parent, Command[] removed, int pos) {
        
    }

    public class LongestCommandFinder implements CommandVisitor {
        public int longest = 0;

        public void visit(Command cmd) {
            if (cmd.getParameterSize() > longest)
                longest = cmd.getParameterSize();

            try {
                Keyword kw = KeywordRegistry.getInstance().createKeyword(cmd.getKeyword());
                if (kw.getParameterSize() > longest)
                    longest = kw.getParameterSize();
            } catch (IllegalKeywordException ignore) {
            }
        }
    }


}
